package com.ncf.jucai.mybatis;

import com.ncf.jucai.mybatis.annotation.PagedEntityExample;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Pattern;


@Intercepts({ @Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class }) })
public class PaginationInterceptor implements Interceptor {

    private static final Logger LGR = LoggerFactory.getLogger(PaginationInterceptor.class);

    /**
     * 拦截后要执行的方法
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = this.getMetaObject(statementHandler);

        // 分离代理对象链(由于目标类可能被多个拦截器拦截，从而形成多次代理，通过下面的两次循环
        // 可以分离出最原始的的目标类)
        while (metaStatementHandler.hasGetter("h")) {
            Object object = metaStatementHandler.getValue("h");
            metaStatementHandler = this.getMetaObject(object);
        }
        // 分离最后一个代理对象的目标类
        while (metaStatementHandler.hasGetter("target")) {
            Object object = metaStatementHandler.getValue("target");
            metaStatementHandler = this.getMetaObject(object);
        }

        // 判断参数里是否有Pagination对象
        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
        BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
        Object obj = boundSql.getParameterObject();
        Pagination page = null;
        Method totalCountSetter = null;
        if (obj != null) {
            if (obj.getClass().getAnnotation(PagedEntityExample.class) != null) {
                String sql = boundSql.getSql();
                if (LIMIT_PTN.matcher(sql).replaceFirst("").length() != sql.length()) {
                    try {
                        totalCountSetter = obj.getClass().getMethod("setTotalCount", Integer.class);
                    } catch (SecurityException|NoSuchMethodException e) {
                        LGR.error("Method[setTotalCount] must exist in target PagedEntityExample::" + obj.getClass());
                    }
                }
            } else if (obj instanceof Pagination) {
                page = (Pagination) obj;
            } else if (obj instanceof Map) {
                Map<?, ?> params = (Map<?, ?>) obj;
                for (Object value : params.values()) {
                    if (value instanceof Pagination) {
                        page = (Pagination) value;
                        break;
                    }
                }
            }
        }

        // 通过Pagination参数作分页操作
        if (page != null || totalCountSetter != null) {
            String sql = boundSql.getSql();
            // 获取总数
            Connection connection = (Connection) invocation.getArgs()[0];
            int totalCount = getTotalItemCount(mappedStatement, connection, boundSql);
            if (page != null) {
                // 给当前的page参数对象设置总记录数
                // this.setTotalRecord(page, mappedStatement, connection);
                // 获取分页Sql语句
                String pageSql = sql + " limit " + page.offset + ", " + page.limit;
                // 利用反射设置当前BoundSql对应的sql属性为我们建立好的分页Sql语句
                MetaObject moBoundSql = this.getMetaObject(boundSql);
                moBoundSql.setValue("sql", pageSql);
                page.setTotalCount(totalCount);
            } else {
                totalCountSetter.invoke(obj, totalCount);
            }
        }

        return invocation.proceed();
    }

    /**
     * 拦截器对应的封装原始对象的方法
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 设置注册拦截器时设定的属性
     */
    @Override
    public void setProperties(Properties properties) {

    }

    private static final Pattern ORDERBY_PTN =
            Pattern.compile("order\\s+by\\s+\\p{Graph}+(\\s+(asc|desc))?(\\s*,\\s*\\p{Graph}+(\\s+(asc|desc))?)*", Pattern.CASE_INSENSITIVE);
    private static final Pattern LIMIT_PTN =
            Pattern.compile("\\s*limit\\s+(\\d+|\\?)(\\s*,\\s*(\\d+|\\?)|\\s+offset\\s+(\\d+|\\?))?", Pattern.CASE_INSENSITIVE);

    /**
     * 获取本次查询的总个数
     *  @param mappedStatement Mapper映射语句
     * @param connection 当前的数据库连接
     */
    private int getTotalItemCount(MappedStatement mappedStatement, Connection connection,
                                  BoundSql boundSql) {
        String sql = boundSql.getSql().replace(System.lineSeparator(), " ");
        sql = ORDERBY_PTN.matcher(sql).replaceFirst("");
        sql = LIMIT_PTN.matcher(sql).replaceFirst("");
        String countSql = "select count(*) from (" + sql + ") inner_query_for_count";
        // 通过BoundSql获取对应的参数映射
        // 利用Configuration、查询记录数的Sql语句countSql、
        // 参数映射关系parameterMappings和参数对象page建立查询记录数对应的BoundSql对象。
        List<ParameterMapping> countParameterMappings = new ArrayList<>();
        for (ParameterMapping pm : boundSql.getParameterMappings()) {
            if ("offset".equals(pm.getProperty()) || "size".equals(pm.getProperty())) {
                continue;
            }
            countParameterMappings.add(pm);
        }
        BoundSql countBoundSql = new BoundSql(
                mappedStatement.getConfiguration(),
                countSql,
                countParameterMappings,
                boundSql.getParameterObject());
        for (ParameterMapping pm : boundSql.getParameterMappings()) {
            String propertyName = pm.getProperty();
            if (boundSql.hasAdditionalParameter(propertyName)) {
                countBoundSql.setAdditionalParameter(propertyName, boundSql.getAdditionalParameter(propertyName));
            }
        }
        // 通过mappedStatement、参数对象page和BoundSql对象countBoundSql建立一个用于设定参数的ParameterHandler对象
        ParameterHandler parameterHandler = new DefaultParameterHandler(
                mappedStatement,
                boundSql.getParameterObject(),
                countBoundSql);

        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            // 通过connection建立一个countSql对应的PreparedStatement对象。
            pstmt = connection.prepareStatement(countSql);
            // 通过parameterHandler给PreparedStatement对象设置参数
            parameterHandler.setParameters(pstmt);
            // 之后就是执行获取总记录数的Sql语句和获取结果了。
            rs = pstmt.executeQuery();
            if (rs.next()) {
                return rs.getInt(1);
            }
        } catch (Exception e) {
            LGR.error("select total count for SqlID[" + mappedStatement.getId() + "] failed.", e);
        } finally {
            try {
                if (rs != null)
                    rs.close();
                if (pstmt != null)
                    pstmt.close();
            } catch (Exception e) {
                LGR.error("", e);
            }
        }
        return -1;
    }

    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    private static final ReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();

    private MetaObject getMetaObject(Object obj) {
        return MetaObject.forObject(
                obj,
                DEFAULT_OBJECT_FACTORY,
                DEFAULT_OBJECT_WRAPPER_FACTORY,
                DEFAULT_REFLECTOR_FACTORY);
    }

}